import { isReactive, isRef, toRaw, unref } from 'vue'
import JSZip from 'jszip'
import * as prettier from 'prettier'
import * as prettierPluginHtml from 'prettier/plugins/html'
import * as prettierPluginBabel from 'prettier/plugins/babel'
import * as prettierPluginEstree from 'prettier/plugins/estree'
import modules from '../modules'

// document.elementsFromPoint(x, y)

// 防抖
export function debounce(fn, wait) {
  let timer = null
  return function (...args) {
    let context = this

    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(context, args)
      timer = null
    }, wait)
  }
}
// 节流
export function throttle(fn, delay) {
  let timer = null

  return function (...args) {
    let context = this

    if (!timer) {
      fn.apply(context, args)
      timer = setTimeout(() => (timer = null), delay)
    }
  }
}
// 生成验证规则
export function generateRule(ruleType, ruleMessage) {
  const rulesObject = {
    none: { required: false },
    default: { required: true, message: ruleMessage, trigger: 'change' },
    phone: {
      required: true,
      validator: new Function(
        'rule',
        'value',
        'callback',
        `if (value === '') {
          callback(new Error('${ruleMessage}'))
        } else {
          if (!/^1[3-9]\\d{9}$/.test(value)) {
            callback(new Error('${ruleMessage}'))
          }
          callback()
        }`
      ),
      trigger: 'blur'
    }
  }
  return rulesObject[ruleType]
}
// 操作历史
export class UndoHistory {
  history = []
  maxLength = 10
  index = -1

  constructor() {}

  push(state) {
    let history = this.history.slice(0, this.index + 1)
    history.push(state)
    this.index = history.length - 1

    if (history.length > this.maxLength) history.shift()

    this.history = history
  }
  // 撤销 fn: (state) => void
  undo(fn) {
    const { index, history } = this

    if (index < 0) return

    const state = index === 0 ? history[0] : history[--this.index]

    fn(state)
  }

  redo(fn) {
    const { index, history } = this
    const length = history.length

    if (index > length - 1) return

    const state = index === length - 1 ? history[length - 1] : history[++this.index]

    fn(state)
  }
}
// 短横线转小驼峰
export function kebab2Camel(property) {
  if (property.includes('--')) return property

  return property.replaceAll(/-(\w)/g, (str, arg) => `${arg.toUpperCase()}`)
}
// 小驼峰转短横线
export function camel2Kebab(property) {
  return property.replaceAll(/[A-Z]/g, (str) => `-${str.toLowerCase()}`)
}
// 对象转样式
export function obj2Style(obj) {
  const propertysArr = []

  for (const [key, value] of Object.entries(obj)) {
    propertysArr.push(
      `${camel2Kebab(key)}: ${value === '' ? 'unset' : value.toString().replaceAll('"', '')}`
    )
  }

  return `{
  ${propertysArr.join(';\n  ')};
}
`
}
// 从全局坐标设置局部位置
export function setElStylePos(el, targetNum, pos /*'left' | 'top'*/, scaleEl) {
  let { [pos]: posNum } = el.getBoundingClientRect()

  // 祖先元素缩放过
  const scale = () => {
    if (!scaleEl) return 1

    const style = window.getComputedStyle(scaleEl)
    const transform = style.getPropertyValue('transform')
    const match = transform.match(
      /matrix\(([-\d\.]+), ([-\d\.]+), ([-\d\.]+), ([-\d\.]+), ([-\d\.]+), ([-\d\.]+)\)/
    )
    let scaleX = +match?.[1] || 1

    return scaleX
  }

  let styleNum = window.getComputedStyle(el).getPropertyValue(pos)
  styleNum = Number.parseFloat(styleNum) || 0

  el.style[pos] = `${(targetNum - posNum) / scale() + styleNum}px`
}

export function isComponent(component) {
  if (component?.render || component?.setup) return true
  return false
}

// 节点树导出json
export function toJson(itemNode) {
  return JSON.stringify(
    itemNode,
    (key, value) => {
      if (isComponent(value)) return undefined
      if (key === 'parent') return undefined
      if (isRef(value)) return unref(value)
      if (isReactive(value)) return toRaw(value)
      return value
    },
    2
  )
}
// json解析成节点树
export function fromJson(json) {
  const getNode = (value) => {
    const NodeClass = modules[value.path]

    if (!NodeClass) return null

    const node = new NodeClass(value)

    return node
  }

  return JSON.parse(json, function (key, value) {
    if (value?.isItemNode) return getNode(value)

    return value
  })
}
// 导出vue文件
export async function toVue(page) {
  const zip = new JSZip()
  const importsFolder = zip.folder('imports')

  let pageSfcObj = null

  page.traverse((node) => {
    const sfcObj = node.toVue()
    if (node.isNPage) pageSfcObj = sfcObj
    else {
      const { uuid, imports, script, importFiles, template, style } = sfcObj

      if (imports) {
        for (const path in imports) {
          const module = imports[path]
          if (pageSfcObj.imports[path]) Object.assign(pageSfcObj.imports[path], module)
          else pageSfcObj.imports[path] = module
        }
      }

      if (script) pageSfcObj.script += script.trim() + '\n\n'

      if (importFiles) Object.assign(pageSfcObj.importFiles, importFiles)
      // 容器子节点uuid替换为模板字符串
      pageSfcObj.template = pageSfcObj.template.replace(uuid, `${template.trim()}`)

      pageSfcObj.style += `\n${style.trim()}\n`
    }
  })

  for (const name in pageSfcObj.importFiles) {
    importsFolder.file(name, pageSfcObj.importFiles[name])
  }

  const toSfc = (strings, imports, script, template, style) => {
    let importsStr = ``

    for (const [key, value] of Object.entries(imports)) {
      const defaultExport = value.default || ''
      const exportsArr = []

      for (const [k, v] of Object.entries(value)) {
        if (k !== 'default') exportsArr.push(v)
      }

      const exports = exportsArr.join(', ')

      importsStr += `import ${defaultExport && defaultExport}${defaultExport && exports && ', '}${exports && `{ ${exports} }`} from '${key}'\n`
    }

    return `${strings[0]}${importsStr}${strings[1]}${script}${strings[2]}${template}${strings[3]}${style}${strings[4]}`
  }

  let pageSfc = toSfc`
<script setup>
import 'element-plus/dist/index.css'
${pageSfcObj.imports}
${pageSfcObj.script}
</script>

<template>
  ${pageSfcObj.template}
</template>

<style scoped>
${pageSfcObj.style}
</style>
  `

  pageSfc = await prettier.format(pageSfc, {
    parser: 'vue',
    plugins: [prettierPluginBabel, prettierPluginEstree, prettierPluginHtml],
    semi: false,
    tabWidth: 2
  })

  zip.file('Page.vue', pageSfc)

  return zip.generateAsync({ type: 'blob' })
}

export async function text2Img(text) {
  const img = new Image()

  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')

  const height = 15
  ctx.font = `${height}px math`
  const width = ctx.measureText(text).width
  const padding = 10
  const doublePadding = padding * 2

  ctx.strokeStyle = 'rgb(0 100 200 / 1)'
  ctx.strokeRect(0, 0, width + doublePadding, height + doublePadding)
  ctx.fillStyle = 'rgb(0 100 200 / 0.5)'
  ctx.fillRect(0, 0, width + doublePadding, height + doublePadding)
  ctx.fillStyle = '#fff'
  ctx.textBaseline = 'top'
  ctx.fillText(text, padding, padding)

  img.src = canvas.toDataURL()

  await new Promise((resolve) => {
    img.onload = resolve
  })

  return img
}
// 文件转base64
export async function file2Base64(file) {
  const reader = new FileReader()

  const result = new Promise((resolve) => {
    reader.addEventListener('load', () => resolve(reader.result))
  })

  reader.readAsDataURL(file)

  return result
}
// 上传文件
export function uploadFile(fn) {
  const input = document.createElement('input')
  input.setAttribute('type', 'file')
  input.click()
  input.onchange = function () {
    fn?.(input.files[0])
  }
}
// 下载文件
export function downloadFile(fileBits, name = '') {
  const file = new File([fileBits], name)
  const url = URL.createObjectURL(file)
  const a = document.createElement('A')
  a.download = name
  a.href = url
  a.click()
  URL.revokeObjectURL(url)
}
// gps坐标转高德坐标
export function gps2Amap([longitude, latitude]) {
  //纬度转换
  function transformLatitude(x, y) {
    let num = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x))
    num += ((20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0) / 3.0
    num += ((20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin((y / 3.0) * Math.PI)) * 2.0) / 3.0
    num +=
      ((160.0 * Math.sin((y / 12.0) * Math.PI) + 320 * Math.sin((y * Math.PI) / 30.0)) * 2.0) / 3.0
    return num
  }
  //经度转换
  function transformLongitude(x, y) {
    let num = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x))
    num += ((20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0) / 3.0
    num += ((20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin((x / 3.0) * Math.PI)) * 2.0) / 3.0
    num +=
      ((150.0 * Math.sin((x / 12.0) * Math.PI) + 300.0 * Math.sin((x / 30.0) * Math.PI)) * 2.0) /
      3.0
    return num
  }
  // 坐标转换
  function calculation(longitude, latitude) {
    let a = 6378245.0 // 卫星椭球坐标投影到平面地图坐标系的投影因子。
    // eslint-disable-next-line @typescript-eslint/no-loss-of-precision
    let ee = 0.00669342162296594323 // 椭球的偏心率。
    let lat = transformLatitude(longitude - 105.0, latitude - 35.0)
    let lng = transformLongitude(longitude - 105.0, latitude - 35.0)
    let radLat = (latitude / 180.0) * Math.PI
    let magic = Math.sin(radLat)
    magic = 1 - ee * magic * magic
    let sqrtMagic = Math.sqrt(magic)
    lat = (lat * 180.0) / (((a * (1 - ee)) / (magic * sqrtMagic)) * Math.PI)
    lng = (lng * 180.0) / ((a / sqrtMagic) * Math.cos(radLat) * Math.PI)
    return {
      longitude: lng,
      latitude: lat
    }
  }
  // 判断是否为国外坐标
  function isOutOfChina(longitude, latitude) {
    if (longitude < 72.004 || longitude > 137.8347) {
      return true
    }
    if (latitude < 0.8293 || latitude > 55.8271) {
      return true
    }
    return false
  }
  // GPS坐标 转 高德坐标
  function getCoordinate(longitude, latitude) {
    let lng = Number(longitude)
    let lat = Number(latitude)
    if (isOutOfChina(longitude, lat)) {
      //国外
      return [lng, lat]
    } else {
      //国内
      let obj = calculation(lng, lat)
      return [lng + obj.longitude, lat + obj.latitude]
    }
  }

  return getCoordinate(longitude, latitude)
}
// 百度坐标转高德坐标
export function baidu2Amap([lng, lat]) {
  let x_pi = (Math.PI * 3000.0) / 180.0
  let x = lng - 0.0065
  let y = lat - 0.006
  let z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi)
  let theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi)
  let lngs = z * Math.cos(theta)
  let lats = z * Math.sin(theta)

  return [lngs, lats]
}
// 获取祖先元素的可见框
export function getVisibleRect(el) {
  const parent = el.offsetParent
  const style = window.getComputedStyle(parent)

  if (style.getPropertyValue('overflow') !== 'visible' || parent.tagName === 'BODY')
    return parent.getBoundingClientRect()
  else return getVisibleRect(parent)
}

export function generateUUID() {
  if (window.crypto?.randomUUID) return crypto.randomUUID()

  const _lut = [
    '00',
    '01',
    '02',
    '03',
    '04',
    '05',
    '06',
    '07',
    '08',
    '09',
    '0a',
    '0b',
    '0c',
    '0d',
    '0e',
    '0f',
    '10',
    '11',
    '12',
    '13',
    '14',
    '15',
    '16',
    '17',
    '18',
    '19',
    '1a',
    '1b',
    '1c',
    '1d',
    '1e',
    '1f',
    '20',
    '21',
    '22',
    '23',
    '24',
    '25',
    '26',
    '27',
    '28',
    '29',
    '2a',
    '2b',
    '2c',
    '2d',
    '2e',
    '2f',
    '30',
    '31',
    '32',
    '33',
    '34',
    '35',
    '36',
    '37',
    '38',
    '39',
    '3a',
    '3b',
    '3c',
    '3d',
    '3e',
    '3f',
    '40',
    '41',
    '42',
    '43',
    '44',
    '45',
    '46',
    '47',
    '48',
    '49',
    '4a',
    '4b',
    '4c',
    '4d',
    '4e',
    '4f',
    '50',
    '51',
    '52',
    '53',
    '54',
    '55',
    '56',
    '57',
    '58',
    '59',
    '5a',
    '5b',
    '5c',
    '5d',
    '5e',
    '5f',
    '60',
    '61',
    '62',
    '63',
    '64',
    '65',
    '66',
    '67',
    '68',
    '69',
    '6a',
    '6b',
    '6c',
    '6d',
    '6e',
    '6f',
    '70',
    '71',
    '72',
    '73',
    '74',
    '75',
    '76',
    '77',
    '78',
    '79',
    '7a',
    '7b',
    '7c',
    '7d',
    '7e',
    '7f',
    '80',
    '81',
    '82',
    '83',
    '84',
    '85',
    '86',
    '87',
    '88',
    '89',
    '8a',
    '8b',
    '8c',
    '8d',
    '8e',
    '8f',
    '90',
    '91',
    '92',
    '93',
    '94',
    '95',
    '96',
    '97',
    '98',
    '99',
    '9a',
    '9b',
    '9c',
    '9d',
    '9e',
    '9f',
    'a0',
    'a1',
    'a2',
    'a3',
    'a4',
    'a5',
    'a6',
    'a7',
    'a8',
    'a9',
    'aa',
    'ab',
    'ac',
    'ad',
    'ae',
    'af',
    'b0',
    'b1',
    'b2',
    'b3',
    'b4',
    'b5',
    'b6',
    'b7',
    'b8',
    'b9',
    'ba',
    'bb',
    'bc',
    'bd',
    'be',
    'bf',
    'c0',
    'c1',
    'c2',
    'c3',
    'c4',
    'c5',
    'c6',
    'c7',
    'c8',
    'c9',
    'ca',
    'cb',
    'cc',
    'cd',
    'ce',
    'cf',
    'd0',
    'd1',
    'd2',
    'd3',
    'd4',
    'd5',
    'd6',
    'd7',
    'd8',
    'd9',
    'da',
    'db',
    'dc',
    'dd',
    'de',
    'df',
    'e0',
    'e1',
    'e2',
    'e3',
    'e4',
    'e5',
    'e6',
    'e7',
    'e8',
    'e9',
    'ea',
    'eb',
    'ec',
    'ed',
    'ee',
    'ef',
    'f0',
    'f1',
    'f2',
    'f3',
    'f4',
    'f5',
    'f6',
    'f7',
    'f8',
    'f9',
    'fa',
    'fb',
    'fc',
    'fd',
    'fe',
    'ff'
  ]
  const d0 = (Math.random() * 0xffffffff) | 0
  const d1 = (Math.random() * 0xffffffff) | 0
  const d2 = (Math.random() * 0xffffffff) | 0
  const d3 = (Math.random() * 0xffffffff) | 0
  const uuid =
    _lut[d0 & 0xff] +
    _lut[(d0 >> 8) & 0xff] +
    _lut[(d0 >> 16) & 0xff] +
    _lut[(d0 >> 24) & 0xff] +
    '-' +
    _lut[d1 & 0xff] +
    _lut[(d1 >> 8) & 0xff] +
    '-' +
    _lut[((d1 >> 16) & 0x0f) | 0x40] +
    _lut[(d1 >> 24) & 0xff] +
    '-' +
    _lut[(d2 & 0x3f) | 0x80] +
    _lut[(d2 >> 8) & 0xff] +
    '-' +
    _lut[(d2 >> 16) & 0xff] +
    _lut[(d2 >> 24) & 0xff] +
    _lut[d3 & 0xff] +
    _lut[(d3 >> 8) & 0xff] +
    _lut[(d3 >> 16) & 0xff] +
    _lut[(d3 >> 24) & 0xff]

  // .toLowerCase() here flattens concatenated strings to save heap memory space.
  return uuid.toLowerCase()
}
